
Day 04 記錄了那次記憶體壓力事件的「現場」,今天把它翻譯成 K8s 的語言。目的不是說「上了 K8s 就不會再發生」,而是搞清楚:同樣的事在 K8s 裡會長什麼樣、可以在哪裡看到它,以及哪些問題就算換到 K8s 也救不了。
那次事件的核心症狀是 swap 塞滿。事後我又發現另一個更隱蔽的現象:RAM 明明還有 17–21 GiB 可用,swap 卻卡在 3.9 / 4.0 GiB。那是先前壓力尖峰留下來的殘留,kernel 不會主動把它搬回 RAM。
根因追到最後有點意外:這台機器的實體記憶體從早期的 8 GB、16 GB 一路加到 32 GB,但 vm.swappiness 一直是 Linux 預設的 60。這個預設值的假設是「記憶體很緊,要早點騰空間」;到了 32 GB 的機器上,就變成「RAM 明明還很多,系統卻提早把分頁寫進 swap」。後來我把它調成 10,並把 swapfile 從 4 GB 加到 8 GB,兩件事都不用重開機。
為什麼在 K8s 系列要提這個?因為 K8s 傳統上要求關掉 swap(kubelet 預設要求 swap off,較新版本開始支援 Linux 節點開 swap,但不是主流配置;目前狀態以官方 Nodes 頁的 swap memory 一節為準)。原因就是我踩到的那件事:swap 讓 kubelet 的記憶體帳算和 limit 執行變得不可預測,排程跟上限都失去依據。
這反過來給了我一個啟發:在沒有 swap 的世界裡,記憶體上限(有設 limit 的前提下)是硬的,超標就被殺,不會「慢慢變卡」。 看起來殘酷,但比較誠實。compose 時代那些「系統怎麼愈來愈慢」的問題,很多其實是 swap 在幫忙掩蓋。
用 K8s 的眼睛重看 Day 04 的幾個角色:
多個 AI coding agent session,各佔 0.4–1.6 GiB。 在 K8s 裡它們會是一個個 Pod。關鍵問題是:有沒有設 requests?requests 和 limits 都沒設的話,scheduler 會當它們幾乎不佔資源,可能全塞進同一個 node。這其實就是那台機器當時的困境:排程的那一端,完全沒有整體用量的視野。
兩個 headless Chrome renderer,42–47% CPU 跑了好幾個小時。 這是「早就該結束卻還在」的工作。翻成 K8s:它們更適合當 Job,跑完就退場;至少也該設 activeDeadlineSeconds,逾時就由系統收掉。在 compose 時代,那兩個 renderer 不是任何 compose 服務的一部分,沒有人管它們的生命週期。
ARM64 QEMU build,CPU 約 212%、持續 swap in / out。 這是 Day 06 的型態二(burst build)。翻成 K8s:用 Job 跑,並且如實填 requests(例如 CPU 2、記憶體 4 GiB),limits 也依峰值填。scheduler 發現 node 放不下時,會讓它 Pending 排隊,而不是硬塞進去拖垮同一台上的所有服務。
閒置 45 小時的 dev 容器,佔 1.25 GiB。 這是最值得玩味的一個。在 K8s 裡如果它是個 Deployment,它一樣會一直待在 node 上,K8s 不會因為「沒人用」就把 Pod 收掉。這是 K8s 也代勞不了的部分:閒置資源的回收是營運政策,不是 scheduler 的工作。差別只在於,K8s 用 kubectl top(前提是叢集有裝 metrics-server)能讓這種浪費一眼看到。
Day 04 提過,Linux 的 OOM killer 挑人的邏輯跟業務優先級無關。K8s 的改善方式是把 Pod 分成三個 QoS class:
| QoS | 條件 | 大致淘汰順序 |
|---|---|---|
| Guaranteed | 所有 container 的 CPU/記憶體 requests = limits | 最後才被殺 |
| Burstable | 有設 requests 但小於 limits | 中間 |
| BestEffort | requests 與 limits 都沒設 | 最先被殺 |
套回上面的角色:核心 bot 設 Guaranteed(用量固定,request = limit 不浪費,也確保不會先被清掉);推論引擎設 Burstable(例如平時 2.5 GiB、峰值 6 GiB);build 和 render 則可以是 BestEffort 或低優先權的 Burstable,反正可以等、可以重跑,先被殺也不傷核心服務。
超過 limit 被殺的容器會標 OOMKilled(節點層級驅逐則是 Evicted),kubectl describe pod 就直接看得到原因,比在主機上翻 dmesg 找 OOM killer 的紀錄友善很多。
老實說,下面這三件 K8s 幫不上忙:
第三點往往是從 compose 走向 K8s 的過渡期最麻煩的盲區:半套的 K8s,常常比純 compose 更難除錯。
明天談 training 跟 inference 為什麼幾乎不該共用一個叢集,是第二週的收尾概念篇。
K8s 沒有消滅記憶體壓力,它只是把「誰在吃多少」變成看得到的數字,把「誰先被殺」變成寫得出來的規則。